Python 51.6%
TypeScript 46.7%
CSS 1.7%
1import type { Metadata } from "next";2import Link from "next/link";3import { notFound } from "next/navigation";4import { apiSafe, type Meta } from "@/lib/api";5import { PartyLogo } from "@/components/party";6import { Badge, Card, Empty } from "@/components/ui";7import { PARTY_IDS, partyVar, partyName } from "@/lib/parties";89export const revalidate = 300;1011interface Proposals { proposals: { id: number; party: string; topic: string; topicLabel: string; subtopic: string | null; proposal: string; position: string | null; direction: string | null; magnitude: string | null; page: number | null; quote: string; confidence: number; document: { id: number; title: string | null; url: string | null; type: string | null } }[] }1213export async function generateMetadata({ params }: { params: Promise<{ topic: string }> }): Promise<Metadata> {14 const { topic } = await params;15 const meta = await apiSafe<Meta>("/api/meta");16 const label = meta?.topics[topic];17 return { title: label ? `${label} — positions des partis` : "Enjeu", description: `Ce que proposent les partis sur ${label ?? "cet enjeu"} d'après leurs documents officiels, avec page et citation.` };18}1920export default async function Page({ params }: { params: Promise<{ topic: string }> }) {21 const { topic } = await params;22 const [meta, data] = await Promise.all([apiSafe<Meta>("/api/meta"), apiSafe<Proposals>(`/api/proposals?topic=${topic}&limit=400`)]);23 if (!meta || !meta.topics[topic]) notFound();24 if (!data) return <Empty title="Indisponible" />;25 const by: Record<string, Proposals["proposals"]> = {};26 data.proposals.forEach((p) => { (by[p.party] ??= []).push(p); });27 return (28 <div className="space-y-6">29 <div><Link href="/enjeux" className="text-[12.5px] text-ink-3 hover:text-ink">← Enjeux</Link><h1 className="display text-[36px] md:text-[48px] mt-2">{meta.topics[topic]}</h1><p className="text-ink-2 mt-2 text-[15px]">{data.proposals.length} propositions extraites des documents officiels. L'absence d'un parti signifie qu'aucun document ingéré n'aborde le sujet.</p></div>30 <div className="grid lg:grid-cols-5 gap-3">31 {PARTY_IDS.map((id) => (32 <div key={id} className="space-y-2">33 <div className="flex items-center gap-2 pb-2 border-b-2" style={{ borderColor: partyVar(id) }}><PartyLogo id={id} height={16} /><span className="font-semibold text-[13px]">{partyName(id)}</span><span className="num text-[12px] text-ink-3 ml-auto">{by[id]?.length ?? 0}</span></div>34 {(by[id] ?? []).length === 0 && <div className="text-[12.5px] text-ink-3 p-2">Aucune proposition documentée.</div>}35 {(by[id] ?? []).map((p) => (36 <Card key={p.id} className="!p-3 text-[12.5px] leading-snug" >37 <div className="font-medium">{p.proposal}</div>38 {p.magnitude && <div className="num text-ink-2 mt-1">{p.magnitude}</div>}39 <blockquote className="mt-2 italic text-ink-2 border-l-2 pl-2" style={{ borderColor: partyVar(id) }}>« {p.quote.length > 240 ? p.quote.slice(0, 240) + "…" : p.quote} »</blockquote>40 <div className="mt-2 flex items-center justify-between gap-2 text-[11.5px] text-ink-3"><Link href={`/documents/${p.document.id}#p${p.id}`} className="link truncate">{p.document.title ?? "Document"}</Link><span className="num shrink-0">{p.page ? `p. ${p.page} · ` : ""}{Math.round(p.confidence * 100)} %</span></div>41 {p.direction && p.direction !== "neutre" && <div className="mt-1.5"><Badge>{p.direction.replace(/_/g, " ")}</Badge></div>}42 </Card>43 ))}44 </div>45 ))}46 </div>47 </div>48 );49}50